import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;




public class ViewPort {
	private Canvas canvasPeer;

	private String expression;
	private double x_axis_scale=0.03;
	private double y_axis_scale=0.03;
	private double x_axis,y_axis;
	private BufferedImage backBuffer;
	private BufferedImage functionImage;
	private Dimension size;
	private Point center_coord;
	private CalculateThread renderMan;
	public double pix_depth[][];
	public int pix[];
	private Calculate calculate;
	
	private class CalculateThread extends Thread{
		private Object lock = new Object();
		private boolean rendering = false;

		public boolean isRendering(){
			return rendering;
		}
		public void callRender(){
			if(rendering)
				return;
			synchronized(lock){
				rendering=true;
				lock.notifyAll();
			}
		}
		public void run(){
			synchronized(lock){
				while(true){
					try {
						lock.wait();
						long stamp=System.currentTimeMillis();
						ViewPort.this.render();
						System.out.println("Taken:"+(System.currentTimeMillis()-stamp));
						rendering=false;
					} catch (Exception e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		}
	}
	private class LittleCanvas extends Canvas{
		public LittleCanvas(){
		}

		public void paint(Graphics g){
			g.drawImage(backBuffer,0,0,this);
		}
	}
	
	public ViewPort(){
		renderMan = new CalculateThread();
		renderMan.start();
	}
	
	public void setSize(Dimension size){
		this.size=size;
	}
	
	
	public Point getCenter_coord() {
		return center_coord;
	}

	public void setCenter_coord(Point center_coord) {
		this.center_coord = center_coord;
	}

	public BufferedImage getBackBuffer() {
		return backBuffer;
	}

	public Dimension getSize() {
		return size;
	}

	public String getExpression() {
		return expression;
	}

	public void setExpression(String expression) {
		this.expression = expression;
	}
	
	private double findBinary(double threshold,double scale){
		double n=1;
		if(n>=threshold*scale){
			while(n/2>=threshold*scale){
				n/=2;
			}
		}else if(n<threshold*scale){
			while(n<threshold*scale){
				n*=2;
			}
		}
		return n;
	}
	
	private void drawAxis(Graphics g){
		final Color axis_color = new Color(200,200,200);
		final Color num_color = new Color(100,100,100);
		final double num_disp_thresh_x = 10 ;
		final double num_disp_thresh_y = 10 ;
		final int num_space=4;
		double axis_space_x = findBinary(num_disp_thresh_x,x_axis_scale);
		double axis_space_y = findBinary(num_disp_thresh_y,y_axis_scale);
		
		g.setColor(axis_color);
		g.drawLine(0, (int)center_coord.getY(), (int)size.getWidth(), (int)center_coord.getY());
		g.drawLine((int)center_coord.getX(),0, (int)center_coord.getX(), (int)size.getHeight());
		
		for(double x=0,xc=1;x/x_axis_scale<size.getWidth()-center_coord.getX();x+=axis_space_x,xc++){
			if((int)(xc)%num_space==0){
				g.setColor(num_color);
				g.drawString(""+x, (int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY()-10);
				g.drawLine((int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY(), (int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY()-5);
			}
			else{
				g.setColor(axis_color);
				g.drawLine((int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY(), (int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY()-3);
			}
		}
	
		for(double x=-axis_space_x,xc=1;x/x_axis_scale>-center_coord.getX();x-=axis_space_x,xc++){
			
			if((int)(xc)%num_space==0){		
				g.setColor(num_color);
				g.drawString(""+x, (int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY()-10);
				g.drawLine((int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY(), (int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY()-5);
			}else{
				g.setColor(axis_color);
				g.drawLine((int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY(), (int)(x/x_axis_scale+center_coord.getX()), (int)center_coord.getY()-3);
			}
		}
		
		for(double y=0,xc=1;y/y_axis_scale<size.getHeight()-center_coord.getY();y+=axis_space_y,xc++){
			
			if((int)(xc)%num_space==0){
				g.setColor(num_color);
				g.drawString(""+y, (int)center_coord.getX()+10, (int)(y/y_axis_scale+center_coord.getY()));
				g.drawLine((int)center_coord.getX(),(int)(y/y_axis_scale+center_coord.getY()), (int)center_coord.getX()+5, (int)(y/y_axis_scale+center_coord.getY()));
			}
			else{
				g.setColor(axis_color);
				g.drawLine((int)center_coord.getX(),(int)(y/y_axis_scale+center_coord.getY()), (int)center_coord.getX()+3, (int)(y/y_axis_scale+center_coord.getY()));
			}
		}
	
		for(double y=-axis_space_y,xc=1;y/y_axis_scale>-center_coord.getY();y-=axis_space_y,xc++){
			
			if((int)(xc)%num_space==0){
				g.setColor(num_color);
				g.drawString(""+y, (int)center_coord.getX()+10, (int)(y/y_axis_scale+center_coord.getY()));
				g.drawLine((int)center_coord.getX(),(int)(y/y_axis_scale+center_coord.getY()), (int)center_coord.getX()+5, (int)(y/y_axis_scale+center_coord.getY()));
			}else{
				g.setColor(axis_color);
				g.drawLine((int)center_coord.getX(),(int)(y/y_axis_scale+center_coord.getY()), (int)center_coord.getX()+3, (int)(y/y_axis_scale+center_coord.getY()));
			}
		}
	}


	
	private void checkAndUpdateBackBuffer(){
		if(backBuffer==null
				||
				size.getWidth()!=backBuffer.getWidth()
				||
				size.getHeight()!=backBuffer.getHeight()){
			backBuffer = new BufferedImage((int)size.getWidth(),(int)size.getHeight(),BufferedImage.TYPE_INT_ARGB);
			functionImage = new BufferedImage((int)size.getWidth(),(int)size.getHeight(),BufferedImage.TYPE_INT_ARGB);
			pix = new int[(int)size.getWidth()*(int)size.getHeight()];
		}
	}
	
	private synchronized void drawFunction(int phy_width,int phy_height,int pixel_size,double x_scale,double y_scale,Graphics2D g) {
		int width = (int)Math.ceil((double)phy_width/(double)pixel_size);
		int height = (int)Math.ceil((double)phy_height/(double)pixel_size);
		pix_depth = new double[width+1][height+1];
		int center_x,center_y;
		center_x = (int)(center_coord.getX());
		center_y = (int)(center_coord.getY());
		if(calculate!=null){
			for(int x=0;x<=width;x++){
				this.x_axis = (x*pixel_size-center_x)*x_axis_scale;
				for(int y=0;y<=height;y++){
					this.y_axis = -(y*pixel_size-center_y)*y_axis_scale;
					//double ev = mathPeer.getValue();
					//double ev = y_axis-x_axis*x_axis*x_axis+2*x_axis*x_axis-x_axis+0.125;
					//double ev= y_axis*y_axis-x_axis*x_axis+100;
					//double ev= Math.sin(y_axis)-x_axis*x_axis;
					//double ev=Math.sin(y_axis)*Math.sin(1.3*y_axis)+Math.cos(x_axis)-x_axis*0.05;
					double ev = calculate.calculate(x_axis,y_axis);
					if(x>width||y>height)
						System.currentTimeMillis();
					pix_depth[x][y]=ev;
				}
			}
		}
		//Color dot = new Color(255,0,0);
		//Color back = new Color(255,255,255);
		int dot = 0xffff0000;
		int back = 0xffffffff;
		for(int y=1,d=0;y<=height;y++,d+=width){
			for(int x=1,c=0;x<=width;x++,c++)
			
				if(pix_depth[x-1][y]*pix_depth[x][y]<=0
						||
						pix_depth[x][y-1]*pix_depth[x][y]<=0
						)
				{
					pix[c+d]=dot;
				}else
					pix[c+d]=back;
				//g.fillRect((x-1)*pixel_size, (y-1)*pixel_size, pixel_size, pixel_size);
			}
		backBuffer.setRGB(0, 0, width, height, pix, 0, width);		
	}

	public void render() throws Exception{
		
		checkAndUpdateBackBuffer();
		

			Graphics2D g=backBuffer.createGraphics();
			g.setTransform(new AffineTransform(
					1,0,
					0,-1,
					0,(int)size.getHeight()
				));
			drawFunction(
					(int)size.getWidth(),
					(int)size.getHeight(),
					1,
					x_axis_scale,
					y_axis_scale,
					g);
			drawAxis(backBuffer.createGraphics());
			canvasPeer.repaint();

	}

	public Canvas getCanvasPeer() {
		return canvasPeer;
	}

	public void setCanvasPeer(Canvas canvasPeer) {
		this.canvasPeer = canvasPeer;
		
	}

	public double getX_axis_scale() {
		return x_axis_scale;
	}

	public void setX_axis_scale(double x_axis_scale) {
		this.x_axis_scale = x_axis_scale;
	}

	public double getY_axis_scale() {
		return y_axis_scale;
	}

	public void setY_axis_scale(double y_axis_scale) {
		this.y_axis_scale = y_axis_scale;
	}


	
	public class Monitor extends JLabel implements MouseMotionListener,MouseWheelListener,MouseListener{
		int mx,my;


		{
			mx=-1;
			my=-1;
		}
		public void mouseDragged(MouseEvent e) {
			// TODO Auto-generated method stub
			if(mx==-1){
				mx = e.getX();
				my = e.getY();
				return;
			}
			if(renderMan.isRendering())
				return;
			int dx,dy;
			dx = e.getX()-mx;
			dy = e.getY()-my;
			
			int cx,cy;
			cx = (int)center_coord.getX();
			cy = (int)center_coord.getY();
			center_coord.setLocation(cx+dx, cy+dy);
			mx = e.getX();
			my = e.getY();			
			renderMan.callRender();
		}

		public void mouseMoved(MouseEvent e) {
			// TODO Auto-generated method stub
			int mouse_x=e.getX();
			int mouse_y=e.getY();
			try{
				this.setText(""+ViewPort.this.pix_depth[mouse_x][(int)size.getHeight()-mouse_y]+"@"+mouse_x+","+mouse_y+" ~x_scale="+ViewPort.this.x_axis_scale+" ~y_scale="+ViewPort.this.y_axis_scale);
			}catch(Exception exp){
				this.setText("Information Not Valid");
			}
		}

		public void mouseWheelMoved(MouseWheelEvent e) {
			// TODO Auto-generated method stub
			if(ViewPort.this.renderMan.isRendering())
				return;
			 double sa = e.getWheelRotation();
			 if(Math.abs(sa)>=2.8)
				 sa = sa/Math.abs(sa)*2.8;
			ViewPort.this.x_axis_scale*=(1.0+sa/3.0);
			ViewPort.this.y_axis_scale*=(1.0+sa/3.0);
			
			center_coord.setLocation(e.getX()+(center_coord.getX()-e.getX())/(1.0+sa/3.0), e.getY()+(center_coord.getY()-e.getY())/(1.0+sa/3.0));
			try {
				ViewPort.this.renderMan.callRender();
			} catch (Exception e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}

		public void mouseClicked(MouseEvent e) {
			// TODO Auto-generated method stub
			
		}

		public void mouseEntered(MouseEvent e) {
			// TODO Auto-generated method stub
			
		}

		public void mouseExited(MouseEvent e) {
			// TODO Auto-generated method stub
			
		}

		public void mousePressed(MouseEvent e) {
			// TODO Auto-generated method stub
		}

		public void mouseReleased(MouseEvent e) {
			// TODO Auto-generated method stub
			mx=-1;my=-1;
		}
		
	}
	
	private void makeCalculate(String equ){
		ClassPool pool = ClassPool.getDefault();
		CtClass clazz=pool.makeClass("CALC"+System.currentTimeMillis());
		try {
			CtMethod calc = CtNewMethod.make("public double calculate(double x,double y){return "+equ+";}",clazz);
			
			clazz.addMethod(calc);
			clazz.addInterface(pool.get("Calculate"));
			calculate = (Calculate)(clazz.toClass().newInstance());
		} catch (Exception e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
	}
	
	public class EquationInput extends JPanel implements ActionListener{
		private JButton jButton = null;
		private JLabel jLabel1 = null;
		private JTextField jTextField = null;

		/**
		 * This is the default constructor
		 */
		public EquationInput() {
			super();
			initialize();
		}

		/**
		 * This method initializes this
		 * 
		 * @return void
		 */
		private void initialize() {
			jLabel1 = new JLabel();
			jLabel1.setText("=0");
			this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
			this.setSize(300, 23);
			this.setFont(new Font("Times New Roman", Font.PLAIN, 12));
			this.add(getJTextField(), null);
			this.add(jLabel1, null);
			this.add(getJButton(), null);
			jButton.addActionListener(this);
			jTextField.addActionListener(this);
		}

		/**
		 * This method initializes jButton	
		 * 	
		 * @return javax.swing.JButton	
		 */
		private JButton getJButton() {
			if (jButton == null) {
				jButton = new JButton();
				jButton.setText("Draw");
				jButton.setFont(new Font("Times New Roman", Font.BOLD, 12));
			}
			return jButton;
		}

		/**
		 * This method initializes jTextField	
		 * 	
		 * @return javax.swing.JTextField	
		 */
		private JTextField getJTextField() {
			if (jTextField == null) {
				jTextField = new JTextField();
				jTextField.setColumns(15);
			}
			return jTextField;
		}

		public synchronized void actionPerformed(ActionEvent e) {
			// TODO Auto-generated method stub
			String txt = jTextField.getText();
			ViewPort.this.makeCalculate(txt);
			renderMan.callRender();
		}
	}
	
	public static void main(String s[]) throws Exception{
		JFrame frame = new JFrame();
		frame.setSize(new Dimension(800,600));
		frame.setResizable(false);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		ViewPort viewPort = new ViewPort();
		
		Canvas canvas =viewPort.new LittleCanvas();
		frame.getContentPane().add(canvas);
		viewPort.setCanvasPeer(canvas);
		viewPort.setExpression("y-40");
		viewPort.setCenter_coord(new Point(400,300));
		viewPort.setSize(frame.getSize());
		
		frame.setVisible(true);
		
		
		Monitor mon = viewPort.new Monitor();
		canvas.addMouseMotionListener(mon);
		canvas.addMouseWheelListener(mon);
		canvas.addMouseListener(mon);
		JFrame monitor = new JFrame();
		monitor.getContentPane().add(mon);
		monitor.setSize(400, 50);
		monitor.setVisible(true);
		
		EquationInput eqin = viewPort.new EquationInput();
		JFrame eqFrame = new JFrame();
		eqFrame.getContentPane().add(eqin);
		eqFrame.pack();
		eqFrame.setVisible(true);
		
		viewPort.makeCalculate("Math.sin(y)*Math.sin(1.3*y)+Math.cos(x)-x*0.05;");
		viewPort.render();
	}
}

interface Calculate{
	public double calculate(double x,double y);
}
